package xml.pull_xml_api;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import com.google.common.collect.Lists;

public class EventReader {

	private final static List<Integer> notIgnoredEvents = Arrays.asList(
			XMLEvent.START_DOCUMENT, XMLEvent.END_DOCUMENT,
			XMLEvent.START_ELEMENT, XMLEvent.END_ELEMENT, XMLEvent.CHARACTERS);
	private final XMLEventReader xer;
	private final LinkedList<StartTag> tagsStack = new LinkedList<StartTag>();

	public EventReader(Reader reader) throws XMLStreamException {
		xer = XMLInputFactory.newInstance().createXMLEventReader(reader);
	}
	
	public EventReader(InputStream in) throws XMLStreamException {
		this(new InputStreamReader(in));
	}
	
	public EventReader(String text) throws XMLStreamException {
		this(new StringReader(text));
	}

	public boolean hasNext() {
		return xer.hasNext();
	}

	public Event getNextEvent() throws XMLStreamException {
		XMLEvent event = xer.nextEvent();

		while (!notIgnoredEvents.contains(event.getEventType()))
			event = xer.nextEvent();
		
		System.err.println(event);

		switch (event.getEventType()) {
		case XMLEvent.START_DOCUMENT: {
			return new StartDoc(event.getLocation());
		}
		case XMLEvent.END_DOCUMENT: {
			return new EndDoc(event.getLocation());
		}
		case XMLEvent.START_ELEMENT: {
			StartTag startTag = getStartTag(event.asStartElement());
			tagsStack.addFirst(startTag);
			return startTag;
		}
		case XMLEvent.END_ELEMENT: {
			EndElement elem = event.asEndElement();
			QName name = elem.getName();
			tagsStack.removeFirst();
			return new EndTag(name, event.getLocation());
		}
		case XMLEvent.CHARACTERS: {
			Characters characters = event.asCharacters();
			return new Chars(characters.getData(), event.getLocation());
		}
		default:
			throw new XMLStreamException("Unexpected event: "+event);
		}
	}

	public StartTag getNextStart(String localPart, Attr... attrs) throws XMLStreamException {
		return getNextStart(new QName(localPart), attrs);
	}

	public StartTag getNextStart(String namespaceURI, String localPart, Attr... attrs)
			throws XMLStreamException {
		return getNextStart(new QName(namespaceURI, localPart), attrs);
	}

	public StartTag getNextStart(QName name, Attr... attrs) throws XMLStreamException {
		StartTag tag = getNextStart();
		while (tag != null && !tag.matches(name, attrs))
			tag = getNextStart();
		return (tag != null && tag.getName().equals(name)) ? tag : null;
	}

	public StartTag getNextStart() throws XMLStreamException {
		Event event = getNextEvent();
		while (!event.isStartTag() && !event.isEndDoc()) {
			event = getNextEvent();
		}
		if (event.isEndDoc())
			return null;
		else
			return event.asStartTag();
	}

	public EndTag getEndFor(StartTag startTag) throws XMLStreamException {
		while (true) {
			XMLEvent event = xer.nextEvent();
			
			while (!notIgnoredEvents.contains(event.getEventType()))
				event = xer.nextEvent();
			
			System.err.println(event);

			switch (event.getEventType()) {
			case XMLEvent.START_DOCUMENT:
			case XMLEvent.CHARACTERS:
				break;
			case XMLEvent.END_DOCUMENT:
				throw new XMLStreamException("Premature end of document", event.getLocation());
			case XMLEvent.START_ELEMENT:
				tagsStack.addFirst(getStartTag(event.asStartElement()));
				break;
			case XMLEvent.END_ELEMENT:
				EndElement endElem = event.asEndElement();
				QName name = endElem.getName();
				StartTag matchingStartTag = tagsStack.removeFirst();
				assert(matchingStartTag.getName().equals(name));
				if (matchingStartTag == startTag)
					return new EndTag(name, event.getLocation());
				else
					break;
			default:
				throw new XMLStreamException("Unexpected event: "+event, event.getLocation());
			}
		}
	}

	public Elem expandCurrentElem() {
		// TODO Auto-generated method stub
		return null;
	}

	private StartTag getStartTag(StartElement elem) throws XMLStreamException {
		QName name = elem.getName();
		List<Attr> attrs = getAttrs(elem);
		return new StartTag(name, attrs);
	}

	private List<Attr> getAttrs(StartElement elem) {
		List<Attr> res = Lists.newArrayList();
		@SuppressWarnings("rawtypes")
		Iterator it = elem.getAttributes();
		while (it.hasNext()) {
			Attribute staxAttr = (Attribute) it.next();
			res.add(new Attr(staxAttr.getName(), staxAttr.getValue()));
		}
		return res;
	}
}
